package core

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/PuerkitoBio/goquery"
)

type Saveer interface {

	// 将body对应的资源进行存储操作
	// url: 资源下载的原地址, 主要用于记录
	// key: 文件存储key
	// body: 资源文件的[]byte
	SaveFile(url, key string, body []byte) error

	// 返回存储的根地址
	SaveRoot() string
}

type RespDoc interface {

	// 获取url资源地址的response响应
	// url： 需要获取资源response响应的地址
	// client: 用于本次请求的client链接
	GetResponse(url string, client *http.Client) (*http.Response, error)
}

// 全部资源压缩接口
type Ziper interface {

	// 压缩mks列表中key对应的资源文件
	// 返回pid压缩标识，key压缩后的文件key
	Zip(mks []string) (pid, key string, err error)
}

// 日志信息记录接口
type Logger interface {

	// 提供日志信息的记录
	Write(b []byte) (int, error)
}

type Copyer struct {
	DocumentUrl    string            // 抓取的页面地址
	DownErrorFiles []string          // 下载失败的资源地址
	SaveErrorFiles []string          // 保存失败的资源地址
	SuccessMap     map[string]string // 记录下载成功,和上传成功的资源
	HtmlKey        string            // 解析完成后主html页面的文件key
	ZipKey         string            // 全部文件压缩后的文件key
	Pid            string            // 压缩队列id

	client   *http.Client      // 用于本次抓取请求的client, 解析的资源都使用该client下载
	document *goquery.Document // 抓取的页面goquery.Document

	saveer  Saveer  // 文件存储实现
	respdoc RespDoc // 主html文档的response响应获取
	ziper   Ziper   // 压缩接口实现
	log     Logger  // 日志输出记录实现
}

// 创建一个资源抓取Copyer接口实现
func NewCoper(sr Saveer, rd RespDoc, ziper Ziper, log Logger) *Copyer {
	c := Copyer{}
	c.SuccessMap = make(map[string]string)
	c.client = http.DefaultClient
	c.saveer = sr
	c.respdoc = rd
	c.ziper = ziper
	c.log = log
	return &c
}

// 抓取结果返回值结构
type CopyerResult struct {
	PreUrl  string   `json:"preurl"`  // 抓取页面预览地址
	ZipUrl  string   `json:"zipurl"`  // 抓取结果压缩后的压缩文件下载地址
	Pid     string   `json:"pid"`     // 压缩队列id
	DE      []string `json:"de"`      // 下载失败的资源地址
	SE      []string `json:"se"`      // 存储失败的资源地址
	UseTime int64    `json:"usetime"` // 本次抓取使用时间(ms)
}

// 使用copyer接口抓取url对应的资源
func Copy(url string, copyer *Copyer) (cr CopyerResult, err error) {
	copyer.DocumentUrl = url

	starttime := time.Now().UnixNano()
	err = copyer.parse()
	if err != nil {
		return
	}

	cr = CopyerResult{
		PreUrl:  copyer.saveer.SaveRoot() + copyer.HtmlKey,
		ZipUrl:  copyer.saveer.SaveRoot() + copyer.ZipKey,
		Pid:     copyer.Pid,
		DE:      copyer.DownErrorFiles,
		SE:      copyer.SaveErrorFiles,
		UseTime: (time.Now().UnixNano() - starttime) / 100000,
	}
	return
}

// 本方法不是线程安全的
func (c *Copyer) parse() error {
	// 通过RespDoc获取html文档的response响应
	resp, err := c.respdoc.GetResponse(c.DocumentUrl, c.client)
	if err != nil {
		return errors.New("抓取主页面Document失败，请检查Url地址是否正确")
	}
	if !strings.Contains(resp.Header.Get("Content-Type"), "text/html") {
		return errors.New("这不是一个有效的 text/html 地址")
	}

	// 从response中创建goquery.Document
	doc, err := goquery.NewDocumentFromResponse(resp)
	if err != nil {
		return errors.New("获取主页面Document解析器失败")
	}
	c.document = doc

	wg := &sync.WaitGroup{} // 开启多个goroutine进行资源文件的下
	c.js(wg)                // 解析script标签
	c.img(wg)               // 解析img标签
	c.css(wg)               // 解析link标签
	wg.Wait()               // 等待js, img, css资源下载完毕

	// 解析html文档中的style资源, 并返回替换后的[]byte
	html, _ := goquery.OuterHtml(c.document.Selection)
	htmlbody := c.styleusingchange([]byte(html), c.DocumentUrl)

	// 将替换后的html文档存储
	key := upkey(".html")
	err = c.saveer.SaveFile(c.DocumentUrl, key, htmlbody)
	if err != nil {
		return errors.New("存储主页面Document结果失败")
	}
	c.SuccessMap[key] = c.DocumentUrl
	c.HtmlKey = key

	// 压缩全部资源文件为zip压缩文件
	mks := make([]string, 0, len(c.SuccessMap)) // 创建所有资源的key集合
	for k, _ := range c.SuccessMap {
		mks = append(mks, k)
	}
	pid, key, err := c.ziper.Zip(mks)
	if err != nil {
		return errors.New("压缩资源文件失败")
	}
	c.Pid = pid
	c.ZipKey = key

	return nil
}

// 解析document中的script标签, 并将资源文件存储
// 并将文档中的路径替换为新路径
func (c *Copyer) js(wg *sync.WaitGroup) {
	js := c.document.Find("script")
	wg.Add(len(js.Nodes)) // 添加启动的goroutine数量

	// 循环解析的所有script标签
	js.Each(func(i int, sel *goquery.Selection) {
		go c.fetchchange(sel, "src", wg)
	})
}

func (c *Copyer) img(wg *sync.WaitGroup) {
	img := c.document.Find("img")

	wg.Add(len(img.Nodes)) // 添加启动的goroutine数量
	// 循环解析所有img标签中的src
	img.Each(func(i int, sel *goquery.Selection) {
		go c.fetchchange(sel, "src", wg)
	})

	// 不启用延时图片加载
	//	wg.Add(len(img.Nodes)) // 添加启动的goroutine数量
	//	// 循环解析所有img标签中的data-original, 延迟加载图片
	//	// 实现对data-original属性的图片延续加载
	//	img.Each(func(i int, sel *goquery.Selection) {
	//		go c.fetchchange(sel, "data-original", wg)
	//	})
}

// 解析link标签，并解析下载的css资源文件中的资源
// 并下载解析出的资源文件，并替换css文件中资源路径
func (c *Copyer) css(wg *sync.WaitGroup) {
	css := c.document.Find("link")
	wg.Add(len(css.Nodes)) // 添加启动的goroutine数量

	// 循环解析的所有link标签
	css.Each(func(i int, sel *goquery.Selection) {
		go func(wg *sync.WaitGroup, sel *goquery.Selection) {
			defer wg.Done()             // 必须减少一个waitgroup的数量避免线程死锁
			href, _ := sel.Attr("href") // 解析出属性中的资源地址，该地址可能是相对路径
			if href == "" {
				return
			}
			url := realurl(href, c.DocumentUrl) // 获取资源的真实下载地址

			c.log.Write([]byte("<-- [down] " + url))
			// 下载css文件到缓存中
			resp, err := c.client.Get(url)
			if err != nil {
				c.DownErrorFiles = append(c.DownErrorFiles, url)
				return
			}
			body, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				c.DownErrorFiles = append(c.DownErrorFiles, url)
				return
			}
			defer resp.Body.Close()

			// 解析css文件中引用的资源，并替换css文件资源地址
			body = c.styleusingchange(body, url)

			// 将替换后的css文件进行存储
			suffix := getsuffix(resp.Header.Get("Content-Type"), url)
			key := upkey(suffix)
			c.log.Write([]byte("<-- [save] " + url))
			err = c.saveer.SaveFile(url, key, body)
			if err != nil {
				c.SaveErrorFiles = append(c.SaveErrorFiles, url)
				return
			}
			c.SuccessMap[key] = url

			// 替换css文件的属性值
			sel.SetAttr("href", key) // 替换标签中的attr属性资源地址
		}(wg, sel)
	})
}

// 解析body中的资源文件，返回替换路径后的body
// body: 需要解析的文件[]byte
// sourceurl: 需要解析的文件的url地址，用于获取资源的真实下载地址
func (c *Copyer) styleusingchange(body []byte, sourceurl string) []byte {
	tmap := make(map[string]string) // key：css中解析出的资源地址，value：文件保存key
	styles := styleusingurl(body)   // 获取解析出的所有style url地址

	wg := &sync.WaitGroup{}
	wg.Add(len(styles))
	for _, v := range styles {
		go func(url string, tm map[string]string, wg *sync.WaitGroup) {
			defer wg.Done()
			zsurl := realurl(url, sourceurl) // 获取解析出的资源的真实地址
			key, err := c.downsave(zsurl)
			if err != nil {
				return
			}
			tm[url] = key
		}(v, tmap, wg)
	}
	wg.Wait() // 等待解析出的资源上传完毕

	// 替换body中资源路径
	fmt.Printf("%v, %v\n", len(tmap), tmap)
	for i, v := range tmap {
		reg := regexp.MustCompile(regexp.QuoteMeta(i))
		body = reg.ReplaceAllLiteral(body, []byte(v))
	}
	return body
}

// 解析标签中的资源路径，并分析出资源的实际下载地址
// 然后将资源存储
// 并替换document中标签的资源路径存储的新资源地址
// sel：标签对象指针
// attr：标签资源对应的属性
// wg: 线程计数器
func (c *Copyer) fetchchange(sel *goquery.Selection, attr string, wg *sync.WaitGroup) {
	defer wg.Done() // 必须减少一个waitgroup的数量避免线程死锁

	href, _ := sel.Attr(attr) // 解析出属性中的资源地址，该地址可能是相对路径
	if href == "" {
		return
	}
	url := realurl(href, c.DocumentUrl) // 获取资源的真实下载地址

	key, err := c.downsave(url)
	if err != nil { // 如果失败则不进行替换
		return
	}

	sel.SetAttr(attr, key) // 替换标签中的attr属性资源地址
}

// 下载url对应的资源文件, 并进行存储
// 返回存储的文件key, 和error
func (c *Copyer) downsave(url string) (string, error) {
	c.log.Write([]byte(fmt.Sprintf("<-- [down] %s", url)))
	resp, err := c.client.Get(url)
	if err != nil {
		c.DownErrorFiles = append(c.DownErrorFiles, url)
		return "", err
	}

	contentType := resp.Header.Get("Content-Type") // 获取响应Content-Type
	suffix := getsuffix(contentType, url)          // 获取资源后缀名称
	if suffix == ".html" {                         // 如果资源为html文件，则进行了跳转将不再下载
		c.DownErrorFiles = append(c.DownErrorFiles, url)
		return "", errors.New("this source file is html, not down")
	}
	key := upkey(suffix) // 获取文件存储key

	body, err := ioutil.ReadAll(resp.Body) // 读取响应body内容
	if err != nil {
		c.DownErrorFiles = append(c.DownErrorFiles, url)
		c.log.Write([]byte(fmt.Sprintf("--> [down error] %s", url)))
		return "", err
	}
	defer resp.Body.Close()
	c.log.Write([]byte(fmt.Sprintf("--> [down] %s", url)))

	// 存储资源文件
	c.log.Write([]byte(fmt.Sprintf("<-- [save] %s", url)))
	err = c.saveer.SaveFile(url, key, body)
	if err != nil {
		c.SaveErrorFiles = append(c.SaveErrorFiles, url)
		c.log.Write([]byte(fmt.Sprintf("--> [save error] %s", url)))
	} else {
		c.SuccessMap[key] = url
		c.log.Write([]byte(fmt.Sprintf("--> [save] %s", url)))
	}
	return key, err
}
